winbrew_app\operations\install/
plan.rs1use super::Result;
2use super::state;
3use super::{InstallObserver, ResolvedInstallTarget, resolve_install_target, sevenz};
4use crate::models::domains::package::PackageRef;
5use crate::models::domains::shared::DeploymentKind;
6use url::Url;
7
8pub struct InstallPreview {
10 target: ResolvedInstallTarget,
11 inspection: state::InstallTargetInspection,
12 ignore_checksum_security: bool,
13}
14
15pub fn build_install_preview<O: InstallObserver>(
17 ctx: &crate::AppContext,
18 package_ref: PackageRef,
19 ignore_checksum_security: bool,
20 observer: &mut O,
21) -> Result<InstallPreview> {
22 let target = resolve_install_target(ctx, package_ref, |query, matches| {
23 observer.choose_package(query, matches)
24 })?;
25 let conn = crate::database::get_conn()?;
26 let inspection = state::inspect_install_target_with_commands(
27 &conn,
28 &target.package.name,
29 &target.install_dir,
30 target.resolved_commands_json.as_deref(),
31 )?;
32
33 Ok(InstallPreview {
34 target,
35 inspection,
36 ignore_checksum_security,
37 })
38}
39
40pub fn preview_lines(
42 ctx: &crate::AppContext,
43 preview: &InstallPreview,
44 show_temp_root: bool,
45) -> Vec<String> {
46 let mut lines = Vec::new();
47
48 lines.push(format!(
49 "Package: {} {}",
50 preview.target.package.name, preview.target.package.version
51 ));
52 lines.push(format!(
53 "Installer URL: {}",
54 shorten_url(&preview.target.installer.url)
55 ));
56 lines.push(format!(
57 "Download payload: {}",
58 match preview
59 .target
60 .download_path
61 .file_name()
62 .and_then(|value| value.to_str())
63 {
64 Some(file_name) => file_name.to_string(),
65 None => preview.target.download_path.display().to_string(),
66 }
67 ));
68 let engine = preview.target.manifest_engine.as_str();
69 let deployment_kind = preview.target.manifest_deployment_kind.as_str();
70 lines.push(format!("Engine: {engine}"));
71 if preview.target.manifest_deployment_kind
72 != default_deployment_kind_for_engine(preview.target.manifest_engine)
73 {
74 lines.push(format!("Deployment: {deployment_kind}"));
75 }
76 lines.push(format!(
77 "Install dir: {}",
78 preview.target.install_dir.display()
79 ));
80 if show_temp_root {
81 lines.push(format!("Temp root: {}", preview.target.temp_root.display()));
82 }
83 lines.push(format!(
84 "Checksum policy: {}",
85 if preview.ignore_checksum_security {
86 "legacy algorithms allowed"
87 } else {
88 "strict"
89 }
90 ));
91
92 match preview.target.resolved_commands.as_deref() {
93 Some(commands) if !commands.is_empty() => {
94 lines.push(format!("Command shims: {}", commands.join(", ")));
95 }
96 _ => {}
97 }
98
99 if preview.target.runtime_bootstrap_required {
100 lines.push(format!(
101 "7-Zip runtime bootstrap: required for {}",
102 sevenz::sevenz_runtime_dir_from_runtime_root(&ctx.paths.root).display()
103 ));
104 } else {
105 lines.push("7-Zip runtime bootstrap: not required".to_string());
106 }
107
108 match preview.inspection.state {
109 state::InstallTargetState::Ready => {
110 lines.push("Preflight: no blockers found".to_string());
111 }
112 state::InstallTargetState::AlreadyInstalled => {
113 lines.push("Preflight blocker: package is already installed".to_string());
114 }
115 state::InstallTargetState::AlreadyInstalling => {
116 lines.push("Preflight blocker: package is already installing".to_string());
117 }
118 state::InstallTargetState::CurrentlyUpdating => {
119 lines.push("Preflight blocker: package is currently updating".to_string());
120 }
121 state::InstallTargetState::Failed => {
122 lines.push("Preflight: stale failed record will be cleaned up".to_string());
123 }
124 state::InstallTargetState::Orphaned => {
125 lines.push("Preflight: orphaned install directory will be cleaned up".to_string());
126 }
127 }
128
129 for conflict in &preview.inspection.command_conflicts {
130 lines.push(format!(
131 "Preflight blocker: command '{}' is already exposed by package '{}'",
132 conflict.command, conflict.package
133 ));
134 }
135
136 lines
137}
138
139fn shorten_url(raw_url: &str) -> String {
140 let Ok(parsed_url) = Url::parse(raw_url) else {
141 return raw_url.to_string();
142 };
143
144 let Some(host) = parsed_url.host_str() else {
145 return raw_url.to_string();
146 };
147
148 let segments = parsed_url
149 .path_segments()
150 .map(|segments| {
151 segments
152 .filter(|segment| !segment.is_empty())
153 .collect::<Vec<_>>()
154 })
155 .unwrap_or_default();
156
157 match segments.as_slice() {
158 [] => host.to_string(),
159 [only] => format!("{host}/{only}"),
160 [.., last] => format!("{host}/.../{last}"),
161 }
162}
163
164fn default_deployment_kind_for_engine(engine: crate::engines::EngineKind) -> DeploymentKind {
165 match engine {
166 crate::engines::EngineKind::Msix
167 | crate::engines::EngineKind::Msi
168 | crate::engines::EngineKind::NativeExe
169 | crate::engines::EngineKind::Font => DeploymentKind::Installed,
170 crate::engines::EngineKind::Zip | crate::engines::EngineKind::Portable => {
171 DeploymentKind::Portable
172 }
173 }
174}